#!/usr/bin/perl -w
#
# LiveCD iso build script
#
# Copyright (C) 2002-2004, Jaco Greeff <jaco@puxedo.org>
# Copyright (C) 2003, Buchan Milne <bgmilne@obsidian.co.za>
# Copyright (C) 2004, Tom Kelly  <tom_kelly33@yahoo.com>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Adapted from the MiniCD (http://www.linuxminicd.org) and mkinitrd build scripts
#
# $Id: mklivecd.in,v 1.164 2006/10/20 03:25:33 ikerekes Exp $
#

use strict;
use lib qw(/usr/lib/libDrakX);

### dependancies
use Getopt::Long;
use MDK::Common;
use run_program;
use Storable qw(store);
use threads::shared;

### useful functions
sub get_exec { my $r = qx($_[0]); chomp($r); $r; };

### global "constants"
my $PROG_VERSION   = '0.6.0-20070511';
my $PROG_NAME      = 'mklivecd';
my $ESC            = "\x1B[";
my $URL            = "http://livecd.berlios.de/";
my $COPYRIGHT      = "Copyright (C) 2002-2004, Jaco Greeff <jaco\@puxedo.org>";

### global variables
my $starttime      = time;
my $workdir        = undef;
my $kernel26       = undef;
my $kernelver      = "2.6.20";
my $dietarch       = undef;
my $depmod         = "/sbin/depmod";
my $modfile        = "/etc/modules.conf";
my $modext         = ".o";
my $modules        = undef;
my $modules_opt    = undef;
my $modules_26     = 'fs/nls/nls_iso8859-1 fs/nls/nls_iso8859-2 fs/nls/nls_cp437 fs/jbd/jbd fs/ext3/ext3 fs/reiserfs/reiserfs fs/fat/fat fs/msdos/msdos fs/vfat/vfat fs/ntfs/ntfs fs/nls/nls_utf8 fs/isofs/isofs drivers/cdrom/cdrom drivers/ide/ide-cd drivers/block/loop  lib/zlib_inflate/zlib_inflate drivers/scsi/aha1542 drivers/ata/ahci drivers/scsi/aic7xxx/aic7xxx drivers/scsi/BusLogic drivers/scsi/dmx3191d drivers/scsi/dtc drivers/scsi/eata drivers/scsi/fdomain drivers/scsi/gdth drivers/scsi/g_NCR5380 drivers/scsi/in2000 drivers/scsi/NCR53c406a drivers/scsi/pas16 drivers/scsi/psi240i drivers/scsi/qlogicfas drivers/scsi/qlogicfc drivers/ata/sata_nv drivers/ata/sata_promise drivers/ata/sata_qstor drivers/ata/sata_sil drivers/ata/sata_sis drivers/ata/sata_svw drivers/ata/sata_sx4 drivers/ata/sata_uli drivers/ata/sata_via drivers/ata/sata_vsc drivers/scsi/sym53c416 drivers/scsi/t128 drivers/scsi/tmscsim drivers/scsi/u14-34f drivers/scsi/wd7000 drivers/block/sx8 drivers/message/fusion/mptscsih drivers/scsi/3w-xxxx drivers/scsi/scsi_mod drivers/scsi/sr_mod drivers/ata/libata drivers/ata/ata_piix drivers/scsi/sg drivers/scsi/sd_mod drivers/scsi/scsi_transport_spi drivers/message/fusion/mptbase drivers/usb/core/usbcore drivers/usb/host/uhci-hcd drivers/usb/host/ohci-hcd drivers/usb/host/ehci-hcd drivers/usb/storage/usb-storage';
my $modules_opt_26 = 'lib/zlib_inflate/zlib_inflate drivers/scsi/3w-xxxx drivers/usb/host/uhci-hcd drivers/usb/host/ohci-hcd drivers/usb/host/ehci-hcd drivers/usb/storage/usb-storage';
my $modules_old26  = 'fs/nls/nls_iso8859-1 fs/nls/nls_iso8859-2 fs/nls/nls_cp437 fs/jbd/jbd fs/ext3/ext3 fs/reiserfs/reiserfs fs/fat/fat fs/msdos/msdos fs/vfat/vfat fs/ntfs/ntfs fs/nls/nls_utf8 fs/isofs/isofs drivers/cdrom/cdrom drivers/ide/ide-cd drivers/block/loop  lib/zlib_inflate/zlib_inflate drivers/scsi/aha1542 drivers/scsi/ahci drivers/scsi/aic7xxx/aic7xxx drivers/scsi/BusLogic drivers/scsi/dmx3191d drivers/scsi/dtc drivers/scsi/eata drivers/scsi/fdomain drivers/scsi/gdth drivers/scsi/g_NCR5380 drivers/scsi/in2000 drivers/scsi/NCR53c406a drivers/scsi/pas16 drivers/scsi/psi240i drivers/scsi/qlogicfas drivers/scsi/qlogicfc drivers/scsi/sata_nv drivers/scsi/sata_promise drivers/scsi/sata_qstor drivers/scsi/sata_sil drivers/scsi/sata_sis drivers/scsi/sata_svw drivers/scsi/sata_sx4 drivers/scsi/sata_uli drivers/scsi/sata_via drivers/scsi/sata_vsc drivers/scsi/sym53c416 drivers/scsi/t128 drivers/scsi/tmscsim drivers/scsi/u14-34f drivers/scsi/wd7000 drivers/block/sx8 drivers/message/fusion/mptscsih drivers/scsi/3w-xxxx drivers/scsi/scsi_mod drivers/scsi/sr_mod drivers/scsi/libata drivers/scsi/ata_piix drivers/scsi/sg drivers/scsi/sd_mod drivers/scsi/scsi_transport_spi drivers/message/fusion/mptbase drivers/usb/core/usbcore drivers/usb/host/uhci-hcd drivers/usb/host/ohci-hcd drivers/usb/host/ehci-hcd drivers/usb/storage/usb-storage';
my $modules_opt_old26 = 'lib/zlib_inflate/zlib_inflate drivers/scsi/3w-xxxx drivers/usb/host/uhci-hcd drivers/usb/host/ohci-hcd drivers/usb/host/ehci-hcd drivers/usb/storage/usb-storage';
my $nodirs         = '^/[.].* ^/dev$ ^/initrd$ ^/lost+found$ ^/mnt$ ^/media$ ^/proc$ ^/sys$ ^/tmp$ ^/var/tmp$ ^/root/tmp$ ^/proc/asound';
my $nofiles        = '^/[.].* ^/fastboot$ /core[.][0-9][0-9]*$ .*~$ .*[.]rpmnew$ .*[.]rpmsave$ [.]bash_history$ [.]fonts[.]cache-[0-9]$ [.]xauth.* [.]wh[.]* [.]xsession-errors$ ^/var/run/ ^/var/lock/subsys/ ^/var/lib/dhcp/ ^/etc/modprobe.conf ^/etc/sysconfig/network-scripts/ifcfg-eth* ^/etc/udev/rules.d/61-* ^/etc/asound.state ^/etc/fstab ^/etc/X11/xorg.conf ^/etc/lilo.conf ^/boot/grub/menu.lst ^/etc/modprobe.preload.d/cpufreq';
my $loopmod        = undef;
my $loopcmp        = undef;
my $o_stage2       = undef;
my $blocksize      = 224;
my $finaliso       = "livecd.iso";
my $loader         = "isolinux/isolinux.bin";
my $md5sum	   = undef;
my $genisoimage_opts   = "";	# Fix for concat of undef in iso looptype

### command-line options
my $o_verbose;
my $o_workdir;
my $o_root         = "/";
my $o_tmp          = "/tmp";
my $o_looptype     = "sqfs";
my $o_keyboard     = 'us';
my $o_resolution   = '800x600';
my $o_vgamode      = '788';
my $o_splash       = "silent";
my $o_bootloader   = "iso";
my $o_ufs          = "aufs";
my $o_kernel       = get_exec("uname -r");
my $o_timeout      = 150;
my %opts           = ( # these are all the options with defaults
	'root'       => \$o_root,
	'tmp'        => \$o_tmp,
	'looptype'   => \$o_looptype,
	'keyboard'   => \$o_keyboard,
	'resolution' => \$o_resolution,
	'splash'     => \$o_splash,
	'kernel'     => \$o_kernel,
	'timeout'    => \$o_timeout,
	'bootloader' => \$o_bootloader,
	'ufs'        => \$o_ufs
);


### startup banner
sub print_banner {
	print STDERR "$PROG_NAME, version $PROG_VERSION, $URL
$COPYRIGHT

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
";
}


### usage
sub print_usage {
	print STDERR "Usage:
   $0 [options] <livecd-image>

General Options:
   --help                          Display this message
   --version                       Display version information
   --verbose                       Be verbose in output
   --noclean                       Don't remove temporary files on exit. #'
   --workdir			   Specify a working directory which will not
   				   be cleaned.
   --debug                         Display some extra debugging information
                                   while building the CD. (Usefull for bug
                                   reports to the developers.)

Image generation:
   --root <rootdir>                Root directory of the live filesystem to use
                                   as the for the image of the LiveCD.
                                   (default: $o_root)
   --tmp <tmpdir>                  Name of the directory to be used for
                                   temporary file storage.
                                   (default: $o_tmp)
   --img <image>                   Name of the saved compressed image. When an
                                   image by this name is found, it will not be
                                   re-created or overwritten, rather the
                                   existing image will be re-used, i.e. the
                                   compressed image is not re-built.
   --nofile <ex1>[,][...]          Excludes files from the final image. (Also
                                   see the --nodir option for a full
                                   description)
   --nodir <ex1>[,][...]           Excludes directories from the final image.
                                   Patterns passed to this option (as with the
                                   --nofile option) should be valid in a grep(1)
                                   search, e.g. --nodir=^/home/jaco,^/root/.mcop
                                   will exclude both the /home/jaco and
                                   /root/.mcop directories from the final
                                   LiveCD.
   --sort <file>                   Sort the files on the compressed iso image
                                   according to the genisoimage-style sort specifier
                                   as detailed in file.
   --kernel <kernel>               Kernel to use as default for the LiveCD
                                   image. (Output of uname -r)
                                   (default: $o_kernel)

Compression options:
   --blocksize <size>              Compression (if used) blocksize in K
                                   (default: $blocksize)
   --looptype <clp|sqfs|iso|ziso>  Create a cloop, squashfs or iso (non-
                                   compressed) image or compressed iso image.
                                   (default: $o_looptype)

Boot options:
   --bootopt <option>              Specify an additional boot option to pass to
                                   the kernel command-line.
   --bootmsg <msg>                 Use 'msg' as the isolinux boot message.
   --bootkey <key=msg>             Display 'msg' on key 'key' from isolinux.
   --bootimg <img>                 Use 'img' (LSS format) as the isolinux.
                                   background display image.
   --bootloader <iso|grub|usb>     The bootloader to use on the livecd i.e. isolinux, 
                                   GRUB or syslinux for usb stick
   --bootmenu <file>               What boot menu definition file should be used
                                   in case that bootloader option is set to iso or grub.
                                   For iso this file must be named 'isolinux.cfg',
				   for grub the name must be 'menu.lst'.
				   Boot menu will be generated if not specified.
   --boottheme <name>              Which gfxboot theme should be used.
                                   Defaults to 'pclinuxos'
   --bootlang <lang code>          Which language shound be used as default
                                   in the boot menu.
				   Defaults to 'en'.
   --ufs <unionfs|aufs>            Specify the union file system.
   --mdkboot                       Use the Mandrake boot image extension to
                                   display the boot message.
   --timeout <sec>                 Specify the default ISO Linux prompt timeout
                                   in seconds.
                                   (default: $o_timeout)
   --noprompt                      Disable ISO Linux prompt (i.e. prompt 0).
   --keyboard <mapping>            Specify a different keyboard layout as
                                   default for the LiveCD.
                                   (default: $o_keyboard)
   --resolution <res>              Specify the resolution for the framebuffer
                                   output device. (Either resolution or normal)
                                   (default: $o_resolution)
   --splash <silent|verbose|no>    Create the LiveCD with bootsplash support if
                                   available on the root filesystem.
                                   (default: $o_splash)
   --fstab <options>               Override the default options for the fstab on
                                   the LiveCD. Options are one or more of 'auto'
                                   and 'rw', for example '--fstab=rw,auto' will
                                   automatically mount all detected partitions
                                   rw.

ISO Image options:
   --isoextrafiles <path>          Add the files in 'path' to the root of the
                                   LiveCD ISO image.
   --application <id>              Use the specified iso application ID, as '-A'
                                   option to genisoimage.
   --volumeid <id>                 Use the specified iso volume ID, as a '-V'
                                   option to genisoimage.
   --preparer <prep>               Use the specified preparer ID, as a '-p'
                                   option to genisoimage.
   --publisher <pub>               Use the specified publisher ID, as a '-P'
                                   option to genisoimage.
   --md5sum                        Compute and implant the md5sum to verify media.
   
Behaviour:
   --usbhome                       Use USB memory stick devices as a persistent
                                   home when available/connected on bootup.

Examples:
    $0 --nodir ^/usr/src/RPM,^/root/tmp livecd.iso
    $0 --blocksize 224 --splash=silent livecd.iso

";
	exit(1);
}


### print our version (already contained in banner)
sub print_version {
	exit(1);
}


### format an elapsed time
sub fmt_elapsed {
	my ($t) = @_;
	my $h = int($t/3600);
	my $m = int(($t - $h*3600)/60);
	my $s = int($t - $h*3600 - $m*60);
	sprintf("%02d:%02d:%02d", $h, $m, $s);
}


### format a number
sub fmt_number {
	my ($n) = @_;
	$n = $n/1000;
	my $t = int($n/1000);
	my $h = $n - $t*1000;
	my $r = sprintf("%0.3f", $h);
	$r =~ s/[.]/,/;
	if ($t > 0) {
		$r = "0$r" while (length($r) < 7);
		$r = "$t,$r";
	}
	$r;
}


### progress bar globals
my $PROGRESS_MAX     = 76;
my $progress_max     : shared = 0;
my $progress_inc     : shared = 0;
my $progress_curr    : shared = 0;
my $progress_start   : shared = 0;
my $progress_tot     : shared = 0;
my $progress_text    : shared = undef;
my $progress_ttext   : shared = undef;
my $progress_timer   : shared = -1; # 0 to active, -1 to deactivate
my $progress_last    : shared = 0;

### to be called before we use the progress bar
sub start_progress {
	my ($title, $max) = @_;
	my $i = 0;
	$| = 1;
	print $title.":\n";
	print "[";
	print " " while ($i++ < $PROGRESS_MAX);
	print "]";
	$progress_max = $max;
	$progress_inc = $progress_max/$PROGRESS_MAX;
	$progress_start = time;
	$progress_curr = 0;
	$progress_tot = 0;
	$progress_text = undef;
	$progress_ttext = undef;
	$progress_last = 0;
	print $ESC."1A".$ESC."52G";
	print "[  0.00% ".fmt_elapsed(0)."/".fmt_elapsed(0)."]";
	start_thread();
}


### while we are progressing
sub set_progress {
	my ($curr, $text, $thr) = @_;
	$progress_last = $curr;
	$progress_last = $progress_max if ($progress_last > $progress_max);
	my $num = int($progress_last/$progress_inc) - $progress_curr;
	if (defined($opts{verbose}) && defined($text)) {
		$text =~ s/\[//g;
		$text =~ s/\]//g;
		$text =~ s/\+//g;
		if (!defined($progress_text) || !($text =~ m/^$progress_text$/)) {
			if (defined($thr) || ($progress_timer == -1)) {
				$progress_text = $text;
				$progress_ttext = $text;
				print "\n";
				$progress_curr += $num;
				print $ESC."1G";
				chomp($text);
				$text =~ s#\r?\n# #gs; # remove newlines in text
				if (length($text) > 72) {
					print "  ".pack("A72", $text)." ...";
				}
				else {
					print "  ".pack("A76",$text);
				}
				print $ESC."1A";
			}
			else {
				$progress_ttext = $text;
			}
		}
	}
	elsif ($num) {
		if (defined($thr) || ($progress_timer == -1)) {
			print "\n";
			my $col = 2+$progress_curr;
			$progress_curr += $num;
			print $ESC.$col."G";
			print "=" while ($num--);
			print $ESC."1A";
		}
	}

	if (defined($thr) || ($progress_timer == -1)) {
		my $elapsed = time - $progress_start;
		my $remain = 0;
		if ($progress_curr < $PROGRESS_MAX) {
			$remain = $progress_curr ? ($elapsed/$progress_curr)*($PROGRESS_MAX - $progress_curr) : 0;
		}
		print $ESC."52G";
		print sprintf("[%6.2f%% ", $curr*100/$progress_max);
		print fmt_elapsed($elapsed)."/".fmt_elapsed($remain+$elapsed)."]";
	}
}


### to close off the progress bar
sub end_progress {
	end_thread();
	set_progress($progress_max, $progress_text);
	print "\n";
	my $i = -2;
	print " " while ($i++ < $PROGRESS_MAX);
	print $ESC."1G";
}


### this is our timer thingy
sub timer_progress {
	while (($progress_timer > 0) && ($progress_timer < 2)) {
		set_progress($progress_last, $progress_ttext, 1);
		sleep(1);
	}
	$progress_timer = 0;
}


### start the progress thread
sub start_thread {
	if ($progress_timer > -1) {
		$progress_timer = 1;
		threads->new(\&timer_progress);
	}
}


### end the progress thread
sub end_thread {
	if ($progress_timer > -1) {
		$progress_timer = 2;
		sleep(2) if ($progress_timer > 0);
	}
}

### parse the command-line options
sub parse_options {
	GetOptions(\%opts,
		'help',
		'version',
		'verbose+',
		'noclean',
		'workdir=s',
		'debug',
		'root=s',
		'tmp=s',
		'img=s',
		'nofile=s@',
		'nodir=s@',
		'sort=s',
		'kernel=s',
		'lowmem',
		'looptype=s',
		'blocksize=i',
		'bootopt=s@',
		'bootmsg=s',
		'bootkey=s%',
		'bootimg=s',
		'mdkboot',
		'timeout=i',
		'noprompt',
		'keyboard=s',
		'resolution=s',
		'splash=s',
		'fstab=s',
		'isoextrafiles=s',
		'application=s',
		'volumeid=s',
		'preparer=s',
		'publisher=s',
		'bootloader=s',
		'bootmenu=s',
		'boottheme=s',
		'bootlang=s',
		'ufs=s',
		'md5sum',
		'usbhome'
	);

	# help and stuff
	print_version if (defined($opts{version}));
	print_usage if (defined($opts{help}));

	# mandatory stuff
	$o_root =~ s|/$||;
	$o_root .= "/";
	die_("\nFATAL: Root directory (--root) '$o_root' does not exist\n") unless (-d $o_root);
	$o_tmp =~ s|/$||;
	$o_tmp .= "/";
	$o_stage2 = get_exec("find $o_root/lib -name stage2_eltorito");
	die_("\nFATAL: Temporary directory (--tmp) '$o_tmp' does not exist\n") unless (-d $o_tmp);
	die_("\nFATAL: Unknown loop type (--looptype) '$o_looptype'\n") unless ($o_looptype =~ /clp|bzlp|sqfs|iso|ziso/);

	# optional stuff
	die_("\nFATAL: Specified sort file (--sort) '".$opts{sort}."' does not exist\n") if (defined($opts{sort}) && !(-f $opts{sort}));
	die_("\nFATAL: Kernel (--kernel) '".$o_kernel."' not installed on the root image. (Directory '".$o_root."/lib/modules/".$o_kernel."' does not exist.)\n") if (!(-d $o_root."/lib/modules/".$o_kernel));
	die_("\nFATAL: Extra ISO directory (--isoextrafiles) '".$opts{isoextrafiles}."' does not exist\n") if (defined($opts{isoextrafiles}) && !(-d $opts{isoextrafiles}));
	die_("\nFATAL: Unknown splash (--splash) option '$o_splash'\n") unless ($o_splash =~ /silent|verbose|no/);
	die_("\nFATAL: Work directory (--workdir) '".$opts{workdir}."' does not exist\n") if (defined($opts{workdir}) && !(-d $opts{workdir}));

	# final iso name
	if (scalar(@ARGV) > 0) {
		die_("\nFATAL: Too many command-line arguments\n") if (scalar(@ARGV) > 1);
		$finaliso = $ARGV[0];
	}
	else {
		die_("\nFATAL: No final iso name specified\nUse --help for usage\n");
	}

	# set some options
	my $rhr = "$o_root/etc/redhat-release";
	my $distro  = (-f $rhr) ? get_exec("gawk -F' ' '{ print \$1 \" \" \$2 }' $rhr") : "$PROG_NAME";
	my $version = (-f $rhr) ? get_exec("gawk -F' ' '{ print \$4 \" \" \$5 }' $rhr") : "$PROG_VERSION";
	my $date    = get_exec("date +\%Y\%m\%d");
	$opts{volumeid} = "$date" unless (defined($opts{volumeid}));
	$opts{application} = "$distro $version LiveCD" unless (defined($opts{application}));
	$opts{preparer} = "$PROG_NAME $PROG_VERSION" unless (defined($opts{preparer}));
	$opts{publisher} = "$URL" unless (defined($opts{publisher}));
	$opts{boottheme} = "pclinuxos" unless (defined($opts{boottheme}));
	$opts{bootlang} = "en" unless (defined($opts{bootlang}));

	# create our working dir if none given
	if ($opts{workdir}) {
		$workdir = $opts{workdir};
		$opts{noclean} = 1;
	} else {
		$workdir = $o_tmp."mklivecd.$$";
	}
	mkdir_p($workdir);
	die_("\nFATAL: Unable to create working directory, '$workdir'\n") unless (-e $workdir);
	print STDERR "\nWARNING: The temporary directory '$workdir' will not be removed at exit, please do so manually" if (defined($opts{noclean}));

	# massage where we might have options csv
	@{$opts{nofile}} = split(/,/, join(',', @{$opts{nofile}})) if (defined($opts{nofile}));
	push @{$opts{nofile}}, split(/ /, $nofiles);
	@{$opts{nodir}} = split(/,/, join(',', @{$opts{nodir}})) if (defined($opts{nodir}));
	push @{$opts{nodir}}, split(/ /, $nodirs);

	# setup executables

	$loopcmp = "/usr/bin/mksquashfs";
	$loopmod = "fs/squashfs/squashfs";
	$blocksize = defined($opts{blocksize}) ? $opts{blocksize} : 32;

	# do other things before starting
	check_kernel();
	check_dietlibc();
	check_resolution();
	check_root();
	check_apps();

	# pretty up
	print "\n\n";
}


### check for the kernel version
sub check_kernel {
	# this should really be perl regex's

	$kernel26 = 1;
	$modfile = "/etc/modprobe.conf";
	$depmod = "/sbin/depmod-25";
	$modext = ".ko";
	if ($o_kernel ge $kernelver) {
	   $modules = $modules_26;
	   $modules_opt = $modules_opt_26;
	}
	else {
	     $modules = $modules_old26;
	     $modules_opt = $modules_opt_old26;
	}
}


### checks if this is a MDK dietlibc architecture
sub check_dietlibc {
	my $arch = get_exec("uname -m");
	$dietarch = 1 if ($arch =~ m/i.86|x86_64/);
}


### checks the resolution given
sub check_resolution {
	if ($o_resolution =~ m/^normal/) {
		$o_vgamode = "normal";
		$o_splash = "no";
	}
	elsif ($o_resolution =~ m/^640x480/) {
		$o_vgamode = 785;
	}
	elsif ($o_resolution =~ m/^800x600/) {
		$o_vgamode = 788;
	}
	elsif ($o_resolution =~ m/^1024x768/) {
		$o_vgamode = 791;
	}
	elsif ($o_resolution =~ m/^1280x1024/) {
		$o_vgamode = 794;
	}
	elsif ($o_resolution =~ m/^1600x1200/) {
		$o_vgamode = 797;
	}
	else {
		die_("\nFATAL: Invalid resolution '$o_resolution' specified with '--resolution' option
       Valid resolutions are:
             normal
            640x480
            800x600
           1024x768
          1280x1024
          1600x1200\n");
	}
}


### see if we are indeed root
sub check_root {
	die_("\nFATAL: You have to be root to execute this program\n") if ($> > 0);
}


### see if we are indeed root
sub check_apps {
	die_("\nFATAL: The '/usr/bin/genisoimage' program is not available, please install the correct package\n") unless (-f '/usr/bin/genisoimage');
	if (($o_bootloader =~m/^grub/) && ($o_stage2 eq '')) {
	    die("\nFATAL: The Grub program is not available, please install the correct package\n")
	}
	die_("\nFATAL: The '$loopcmp' program is not available, please install the correct package\n") unless (!defined($loopcmp) || -f $loopcmp);
}


### execute a command
sub do_cmd {
	my ($cmd, $prog) = @_;
	set_progress($prog-1, $cmd) if (defined($prog));
	system($cmd) and die_("\nFATAL: Execution of '$cmd' failed\n");
	set_progress($prog, $cmd) if (defined($prog));
}


### copy a file
sub do_copy {
	my ($src, $mode, $dest, $prog) = @_;
	my $cmd = ($mode > 0) ? "install -m $mode $src $dest" : "cp -aL $src $dest";
	set_progress($prog-1, $cmd) if (defined($prog));
	system($cmd);
	set_progress($prog, $cmd) if (defined($prog));

	if (-e "/tmp/bootsplash/scripts/make-boot-splash") {
	    do_cmd("rm -rf /tmp/bootsplash");
        }
	do_cmd("mkdir -p /tmp/bootsplash");
	do_cmd("cp -r /usr/share/bootsplash/scripts /tmp/bootsplash");
	do_cmd("perl -pi -e 's#usr/share/bootsplash#tmp/bootsplash#' /tmp/bootsplash/scripts/make-boot-splash");
	do_cmd("perl -pi -e 's/^warn/#warn/' /tmp/bootsplash/scripts/remove-boot-splash");
}   	


### create an initrd image
sub create_initrd {
	# get our modules
	my @allmods = split(/ /, $modules);
	push @allmods, $loopmod if (defined($loopmod));
	my @allmods_opt = split(/ /, $modules_opt);

	# start progress bar
	my $pos = 0;
	start_progress("Creating initrd", 42+scalar(@allmods));

	# create directories
	my $dir = $workdir."/initrd.dir";
	mkdir_p("$dir/$_") foreach "lib/modules/$o_kernel/kernel", qw(bin dev etc/livecd/hwdetect etc/rc.d/ proc ramfs sbin usr/bin usr/sbin cdrom loopfs ramfs);
	set_progress(++$pos, "mkdir -p $dir/...");

	# find our insmod
	if (defined($dietarch)) {
		if (-e "$o_root/sbin/insmod-25")  {do_copy("$o_root/sbin/insmod-25",	755,	"$dir/sbin/insmod-25", ++$pos)}
		if (-e "$o_root/sbin/insmod-24")  {do_copy("$o_root/sbin/insmod-24",	755,	"$dir/sbin/insmod-24", ++$pos)}
		if (-e "$o_root/sbin/insmod.old") {do_copy("$o_root/sbin/insmod.old",	755,	"$dir/sbin/insmod.old", ++$pos)}
		if (defined($kernel26)) {
			do_copy("$dir/sbin/insmod-25",	755,	"$dir/sbin/insmod",	++$pos);
		} else {
			do_copy("$dir/sbin/insmod-24",	755,	"$dir/sbin/insmod",	++$pos);
		}
	} else {
		if (-e "$o_root/sbin/insmod.static") {
			do_copy("$o_root/sbin/insmod.static",	755,	"$dir/sbin/insmod.static", ++$pos);
		}
	}

	# copy files
	do_copy("/usr/bin/busybox",               755, "$dir/bin/busybox",             ++$pos);
	do_copy("/usr/share/mklivecd/linuxrc",    755, "$dir/linuxrc",                 ++$pos);
	do_copy("/usr/share/mklivecd/rc.sysinit", 755, "$dir/etc/rc.d/rc.sysinit",     ++$pos);
        if ($o_ufs =~ m/^unionfs/) {
	    do_cmd("perl -pi -e 's/AUFS/UNIONFS/' $dir/etc/rc.d/rc.sysinit");
	    do_cmd("perl -pi -e 's/aufs/unionfs/' $dir/etc/rc.d/rc.sysinit");
	    do_cmd("perl -pi -e 's/ro none/ro unionfs/' $dir/etc/rc.d/rc.sysinit");
	}
	do_copy("/usr/sbin/hwdetect",             755, "$dir/usr/sbin/hwdetect",       ++$pos);
	do_copy("/usr/share/mklivecd/halt.local", 755, "$dir/sbin/halt.local",         ++$pos);
	do_copy($o_root."etc/inittab",            644, "$dir/etc/inittab",             ++$pos);
	do_copy($o_root."sbin/init",              755, "$dir/sbin/init.dynamic",       ++$pos);
	do_copy($o_root."lib/libc.so.6",            0, "$dir/lib",                     ++$pos);
	do_copy($o_root."lib/ld-linux.so.2",        0, "$dir/lib",                     ++$pos);


	# Copy but hide the udev binaries
	do_cmd("mkdir $dir/sbin/tmp",			++$pos);
	do_copy($o_root."/sbin/udev*",	  755, "$dir/sbin/tmp",			++$pos);
	do_cmd("cp -a $o_root/etc/udev $dir/etc",	++$pos);
	if (-e "$o_root/etc/dev.d") {
	    do_cmd("cp -a $o_root/etc/dev.d $dir/etc",	++$pos);
	}
	do_cmd("cp -a $o_root/etc/hotplug $dir/etc",	++$pos);
	do_cmd("cp -a $o_root/etc/sysconfig $dir/etc",	++$pos);

	do_cmd("mknod $dir/dev/initrd b 1 250", ++$pos);
	do_cmd("mknod $dir/dev/console c 5 1",  ++$pos);
	do_cmd("mknod $dir/dev/null c 1 3",     ++$pos);
	do_cmd("mknod $dir/dev/systty c 4 0",   ++$pos);
	do_cmd("mknod $dir/dev/ram b 1 1",      ++$pos);
	do_cmd("mknod $dir/dev/tty$_ c 4 $_",   ++$pos) foreach((1,2,3,4));
	do_cmd("mknod $dir/dev/lvm b 109 0",    ++$pos);


	# copy modules
	foreach my $mod (@allmods) {
		my $modpath = $o_root."lib/modules/$o_kernel/kernel/$mod$modext";
		$mod =~ s,.*/,, ; ## basename and perl (file::basename)
		my $tomod = "$dir/lib/modules/$o_kernel/kernel/$mod$modext";
		if (-f $modpath) {
			do_copy($modpath, 644, $tomod, ++$pos);
		}
		else {
			$modpath = $modpath.".gz";
			if (-f $modpath) {
				do_cmd("gzip -dc $modpath >$tomod", ++$pos);
			}
			else {
				my $modopt = undef;
				foreach my $opt (@allmods_opt) {
					$opt = "$opt$modext";
					$modopt = 1 if ($opt =~ m/$mod/);
				}
				set_progress(++$pos, $modpath);
			}
		}
	}

	# generate modules file
	do_cmd(":>$dir$modfile", ++$pos);
	my $cfg = defined($kernel26) ? "" : "--config $dir$modfile";
	do_cmd("(depmod -a --basedir $dir ".$cfg." $o_kernel || cp -f $o_root/lib/modules/$o_kernel/modules.dep $dir/lib/modules/$o_kernel) 2>/dev/null", ++$pos);

	# get sizes$o_kernel
	my $size = get_exec("du -ks $dir | awk '{print \$1}'");
	$size = $size + 250;
	set_progress(++$pos, "du -ks $dir");

	# number of inodes
	my $inodes = 1250;
	my $num = get_exec("find $dir | wc -l");
	$inodes = $inodes + $num;
	$size = int($size + ($inodes / 10)) + 1; # 10 inodes needs 1K
	set_progress(++$pos, "find $dir | wc -l");

	# do magic
	my $initrd = "$workdir/livecd/isolinux/initrd";
	my $mnt = "$workdir/initrd.mnt";
	do_cmd("mkdir -p $workdir/livecd/isolinux",                         ++$pos);
	do_cmd("mkdir -p $workdir/livecd/boot/grub",                        ++$pos);
	do_cmd("dd if=/dev/zero of=$initrd bs=1k count=$size 2> /dev/null", ++$pos);
	do_cmd("mke2fs -q -m 0 -F -N $inodes -s 1 $initrd",                 ++$pos);
	do_cmd("mkdir -p $mnt ; mount -o loop -t ext2 $initrd $mnt",        ++$pos);
	do_cmd("rm -rf $mnt/lost+found",                                    ++$pos);
	do_cmd("(cd $dir ; tar cf - .) | (cd $mnt ; tar xf -)",             ++$pos);
	do_cmd("umount $mnt",                                               ++$pos);
	do_cmd("(cd $workdir/livecd/isolinux ; gzip -9 initrd)",            ++$pos);

	# make splash
	if ($o_splash !~ m/no/) {
	do_cmd("[ -d $o_root/tmp ] || mkdir $o_root/tmp ; \
		mv -f $initrd.gz $o_root/tmp ; \
		/tmp/bootsplash/scripts/make-boot-splash $o_root/tmp/initrd.gz $o_resolution; \
		mv -f $o_root/tmp/initrd.gz $workdir/livecd/isolinux", ++$pos);
	}
	# we are done
	end_progress();
}


### create the compressed image
sub create_compressed {
	if (defined($opts{img}) && (-f $opts{img})) {
		do_cmd("ln $opts{img} $workdir/livecd/livecd.$o_looptype");
	}
	else {
		my $pos = 0;
		start_progress("Setting filesystem parameters", 5);

		# handle excludes
		do_cmd(":>$workdir/excludes.list", ++$pos);
		if (defined($opts{nodir})) {
			my $ex = join("\n", @{$opts{nodir}});
			do_cmd("(find $o_root -type d 2>/dev/null | sed -e 's,^$o_root,/,g' | grep '$ex' | sed 's,^/,$o_root,' >>$workdir/excludes.list)", ++$pos);

		}
		if (defined($opts{nofile})) {
			my $ex = join("\n", @{$opts{nofile}});
			do_cmd("(find $o_root -type f 2>/dev/null | sed -e 's,^$o_root,/,g' | grep '$ex' | sed 's,^/,$o_root,' >>$workdir/excludes.list)", ++$pos);
		}

		# handle sort
		do_cmd(":>$workdir/sort.list", ++$pos);
		do_cmd("cat ".$opts{sort}." >>$workdir/sort.list", ++$pos) if (defined($opts{sort}));
		end_progress();

		$pos = 0;
		my $size = $blocksize*1024;
		if ($o_looptype =~ m/sqfs/) {
			my @files = qx(find $o_root -type f 2>/dev/null);
			my $total = scalar(@files);
			start_progress("Creating compressed image", $total+2);
			my $iso = "$loopcmp $o_root $workdir/livecd/livecd.$o_looptype -info -ef $workdir/excludes.list";
			$iso = "$iso -sort $workdir/sort.list" if (defined($opts{sort}));
			$iso = "$iso 2>&1";
			print "DEBUG: $iso\n" if (defined($opts{debug}));
			open CMP, "$iso |" or die_("\nFATAL: Unable to execute '$iso'\n");
			my $line;
			my $linep = "";
			while ($line = <CMP>) {
				if ($line =~ m/mksquashfs: file/) {
					$linep = $line;
					$pos++;
					set_progress($pos, $linep);
				}
				else {
					if (defined($opts{debug})) {
						print "$line\n" if ($line =~ m/Error/);
					}
				}
			}
			close CMP;
			do_cmd("chmod 644 $workdir/livecd/livecd.$o_looptype", $total+1); # Failure = out of space
			do_cmd("ln $workdir/livecd/livecd.$o_looptype $opts{img}", $total+2) if (defined($opts{img}));
			end_progress();
		}
		else {
			start_progress("Creating loop image", 10001);
			my $iso = "genisoimage $genisoimage_opts -R -exclude-list $workdir/excludes.list -hide-rr-moved -cache-inodes -no-bak -pad -v -v";
			$iso = "$iso -sort $workdir/sort.list" if (defined($opts{sort}));


			$iso = "$iso -o $workdir/livecd/livecd.$o_looptype $o_root";

			$iso = "($iso) 2>&1";
			print "DEBUG: $iso\n" if (defined($opts{debug}));
			open CMP, "$iso |" or die_("\nFATAL: Unable to execute '$iso'\n");
			my $line;
			while ($line = <CMP>) {
				if ($line =~ m/done, estimate/) {
					$line =~ s/^ //g while ($line =~ m/^ /);
					my ($per, @rest) = split(/ /, $line);
					$per =~ s/%//;
					$pos = int($per*100);
				}
				set_progress($pos, $line);
			}
			close CMP;
			do_cmd("ln $workdir/livecd/livecd.$o_looptype $opts{img}", 10001) if (defined($opts{img}));
			end_progress();
		}
	}
}


### create the isolinux stuff
sub create_isolinux {
	my $pos = 0;
	start_progress("Creating isolinux boot", 9);

	# copy boot images
	my $bin = "/usr/lib/syslinux/isolinux.bin";
	$bin = "/usr/lib/syslinux/isolinux-graphic.bin" if (defined($opts{mdkboot}));
	die_("\nFATAL: '$bin' does not exist on your machine. You need to install the syslinux package.\n") unless (-f $bin);
	do_copy($bin, 644, "$workdir/livecd/isolinux/isolinux.bin", ++$pos);
	die_("\nFATAL: The kernel '".$o_root."boot/vmlinuz-$o_kernel' does not exist on your machine.\n") unless (-f $o_root."boot/vmlinuz-$o_kernel");
	do_copy($o_root."boot/vmlinuz-$o_kernel", 644, "$workdir/livecd/isolinux/vmlinuz", ++$pos);
	do_copy("/usr/bin/mediacheck", 755, "$workdir/livecd/isolinux/mediacheck", ++$pos);
	do_copy($o_root."boot/memtest*.*.bin", 644, "$workdir/livecd/isolinux/memtest", ++$pos);
        if ($o_bootloader =~ m/^iso/) {
	    do_copy($o_root."usr/share/gfxboot/themes/$opts{boottheme}/install/*", 644, "$workdir/livecd/isolinux", ++$pos);
	  } else {
	    do_copy($o_stage2, 644, "$workdir/livecd/boot/grub/stage2_eltorito", ++$pos);
	    do_copy($o_root."boot/grub/*.xpm.gz", 644, "$workdir/livecd/boot/grub/livecd.xpm.gz", ++$pos);
	    do_copy($o_root."usr/share/gfxboot/themes/$opts{boottheme}/boot/*", 644, "$workdir/livecd/boot/grub", ++$pos);
        }

	# copy messages
	do_copy($opts{bootmsg}, 644, "$workdir/livecd/isolinux/livecd.msg", ++$pos) if (defined($opts{bootmsg}));
	if (defined($opts{bootimg})) {
		do_cmd("echo -n '' >$workdir/livecd/isolinux/livecd.msg", ++$pos);
		do_cmd("echo -e '\\030livecd.lss' >>$workdir/livecd/isolinux/livecd.msg", ++$pos);
		do_copy($opts{bootimg}, 644, "$workdir/livecd/isolinux/livecd.lss", ++$pos);
	}

	# write config
	my $appopt;
	if ($kernel26 == 1) {
		$appopt = "initrd=initrd.gz root=/dev/rd/3 acpi=on vga=$o_vgamode keyb=$o_keyboard";
	} else {
		$appopt = "initrd=initrd.gz root=/dev/rd/3 devfs=mount vga=$o_vgamode keyb=$o_keyboard";
	}
	$appopt .= " splash=$o_splash" if ($o_splash !~ m/no/);
	$appopt = "$appopt fastboot=yes automatic=method:cdrom ramdisk_size=32000" if (!defined($kernel26));
	$appopt = "$appopt fstab=".$opts{fstab} if (defined($opts{fstab}));
	$appopt = "$appopt home=usb" if (defined($opts{usbhome}));
	$appopt = "$appopt $_" foreach (@{$opts{bootopt}});

	open LANG, '>', "$workdir/livecd/isolinux/lang";
	print LANG $opts{bootlang}."\n";
	close LANG;
	
	if (defined($opts{bootmenu}) && -f $opts{bootmenu} )
	{
	    do_copy($opts{bootmenu}, 644, "$workdir/livecd/isolinux", ++$pos);
	} else {
	open CFG, '>', "$workdir/livecd/isolinux/isolinux.cfg";
	print CFG "default livecd\n";
	print CFG "prompt  ".(defined($opts{noprompt}) ? "0" : "1")."\n";
	print CFG "timeout $o_timeout\n";
	print CFG "display livecd.msg\n" if (-f "$workdir/livecd/isolinux/livecd.msg");
	foreach my $k (keys %{$opts{bootkey}}) {
		do_copy(${$opts{bootkey}}{$k}, 644, "$workdir/livecd/isolinux/livecd_$k.msg");
		print CFG "$k livecd_$k.msg\n";
	}
	print CFG "gfxboot bootlogo\n";
	print CFG "label LiveCD\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd $appopt\n";
	print CFG "label VideoSafeModeFBDev\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd $appopt framebuffer\n";
	print CFG "label VideoSafeModeVesa\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd $appopt vesa\n";
	print CFG "label Safeboot\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd initrd=initrd.gz root=/dev/rd/3 acpi=off vga=normal keyb=us noapic nolapic noscsi nopcmcia\n";
	print CFG "label Console\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd 3 $appopt\n";
	print CFG "label Copy2ram\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd copy2ram $appopt splash=verbose\n";
	print CFG "label MediaCheck\n";
	print CFG "    kernel vmlinuz\n";
	print CFG "    append livecd=livecd md5sum $appopt splash=verbose\n";
	print CFG "label Memtest\n";
	print CFG "    kernel memtest\n";	
	close CFG;
	}
	
    if ($o_bootloader =~ m/^grub/) {
	if (defined($opts{bootmenu}) && -f $opts{bootmenu} )
	{
	    do_copy($opts{bootmenu}, 644, "$workdir/livecd/boot/grub", ++$pos);
	} else {
	open CFG, '>', "$workdir/livecd/boot/grub/menu.lst";
	print CFG "default 0\n";
	print CFG "timeout 30\n";
	print CFG "gfxmenu (cd)/boot/grub/message\n\n";
	print CFG "title LiveCD\n";
	print CFG "kernel (cd)/isolinux/vmlinuz livecd=livecd $appopt\n";
	print CFG "initrd (cd)/isolinux/initrd.gz\n\n";
	print CFG "title VideoSafeModeFBDev\n";
	print CFG "kernel (cd)/isolinux/vmlinuz livecd=livecd $appopt framebuffer\n";
	print CFG "title VideoSafeModeVesa\n";
	print CFG "kernel (cd)/isolinux/vmlinuz livecd=livecd $appopt vesa\n";
	print CFG "initrd (cd)/isolinux/initrd.gz\n\n";
	print CFG "title Safeboot\n";
	print CFG "kernel (cd)/isolinux/vmlinuz livecd=livecd root=/dev/rd/3 acpi=off vga=normal keyb=us noapic nolapic noscsi nopcmcia\n";
	print CFG "initrd (cd)/isolinux/initrd.gz\n\n";
	print CFG "title Console\n";
	print CFG "kernel (cd)/isolinux/vmlinuz $appopt 3\n";
	print CFG "initrd (cd)/isolinux/initrd.gz\n\n";
	print CFG "title Copy2ram\n";
	print CFG "kernel (cd)/isolinux/vmlinuz livecd=livecd copy2ram $appopt splash=verbose\n";
	print CFG "initrd (cd)/isolinux/initrd.gz\n\n";
	print CFG "title MediaCheck\n";
	print CFG "kernel (cd)/isolinux/vmlinuz livecd=livecd md5sum $appopt splash=verbose\n";
	print CFG "initrd (cd)/isolinux/initrd.gz\n\n";
	print CFG "title Memtest\n";
	print CFG "kernel (cd)/isolinux/memtest\n\n\n";
	close CFG;
	}
    }

	set_progress(++$pos, "$workdir/livecd/isolinux/isolinux.cfg");

	# create boot catalogue
	do_cmd("dd if=/dev/zero of=$workdir/livecd/isolinux/boot.cat bs=1k count=2 2> /dev/null", ++$pos);

	end_progress();
}


### create the final iso
sub create_finaliso {
	# create a sort-file
	my $pos = 0;
	start_progress("Creating final iso", 10001);
	open SORT, '>', "$workdir/sort_iso.list";
	print SORT "$workdir/livecd/isolinux/isolinux.bin 500\n";
	print SORT "$workdir/livecd/isolinux/isolinux.cfg 499\n";
	print SORT "$workdir/livecd/isolinux/vmlinuz 498\n";
	print SORT "$workdir/livecd/isolinux/initrd.gz 497\n";
	print SORT "$workdir/livecd/isolinux/* 450\n";
	print SORT "$workdir/livecd/livecd.$o_looptype 400\n";
	close SORT;
	set_progress(++$pos, "$workdir/sort_iso.list");

	# create actual iso
	$opts{isoextrafiles} = "" unless (defined($opts{isoextrafiles}));
	if ($o_bootloader =~ m/^grub/) {
		$loader = "boot/grub/stage2_eltorito";
	}
	elsif ($o_bootloader =~ m/^iso/) {
		$loader = "isolinux/isolinux.bin";
	}
	my $cmd="genisoimage -pad -l -R -J -v -v \\\
                  -V '".$opts{volumeid}."' \\\
                  -A '".$opts{application}."' \\\
                  -p '".$opts{preparer}."' \\\
                  -P '".$opts{publisher}."' \\\
                  -b $loader -c isolinux/boot.cat -hide-rr-moved -no-emul-boot -boot-load-size 4 -boot-info-table -sort $workdir/sort_iso.list -o $finaliso $workdir/livecd/ ".$opts{isoextrafiles}." 2>&1";
          open ISO, "$cmd |" or die_("\nFATAL: Unable to execute '$cmd'\n");
          my $line;
          while ($line = <ISO>) {
		if ($line =~ m/done, estimate/) {
			$line =~ s/^ //g while ($line =~ m/^ /);
			my ($per, @rest) = split(/ /, $line);
			$per =~ s/%//;
			$pos = int($per*100)+1;
		}
		set_progress($pos, $line);
	}
	close ISO;
	end_progress();
}


### create the embedded md5sum
sub create_md5 {
	my $isosize = get_exec("ls -al --block-size=1M $finaliso | awk '{print \$5 }'");
	start_progress("Embedding MD5 checksum", $isosize);
	open MD5, "implantisomd5 $finaliso |" or die_("\nFATAL: Unable to execute 'implantisomd5'\n");
	my $line;
	my $pos = 0;
	while ($line = <MD5>) {
		chomp($line);
		if ($line =~ /^Read/) {
			$pos = $line;
			$pos =~ s/Read//;
			$pos =~ s/MB//;
			$pos =~ s/\s// while ($pos =~ /\s/);
		}
		set_progress($pos, $line);
	}
	close MD5;
	end_progress();
}


### clean everything
sub cleanup {
	if (defined($workdir)) {
		system("umount $workdir/initrd.mnt 2>/dev/null");
		system("rm -rf $workdir") unless (defined($opts{noclean}));
	}
	    if (-e "/tmp/bootsplash/scripts/make-boot-splash") {
		do_cmd("rm -rf /tmp/bootsplash");
	    }
}


### signals
sub die_ {
	die('DIE', join(' ', @_));
}
sub do_signal {
	my ($signal, $text) = @_;
	end_thread();
	my $to = $text ? $text : "\nFATAL: Interrupted.\n";
	chomp($to);
	print pack("A80", $to)."\n";
	cleanup();
	exit(1);
}


### main program entry point
MAIN: {
	$SIG{INT} = \&do_signal;
	$SIG{KILL} = \&do_signal;
	$SIG{PIPE} = \&do_signal;

	print_banner();
	parse_options();

	create_initrd();

	create_compressed();
	create_isolinux();
	create_finaliso();
	create_md5() if (defined($opts{md5sum}));;
	cleanup();

	my $finalsize = get_exec("ls -al $finaliso | awk '{print \$5 }'");
	print "\nCreated '$finaliso' (".fmt_number($finalsize)." bytes) in ".fmt_elapsed(time-$starttime)."\n\n";
}
